/* ***** BEGIN LICENSE BLOCK *****
 *   Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 * 
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is JsWs.
 *
 * The Initial Developer of the Original Code is
 * Chris Winberry.
 * Portions created by the Initial Developer are Copyright (C) 2007
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 * 
 * ***** END LICENSE BLOCK ***** */

/**
 * @class Exception class for server side script parsing
 * @constructor
 * @base ExBase
 * @param {String} msg Description of exception
 * @param {Number} code (optional) Error code
 * @param {Exception} ex (optional) An earlier exception to wrap
 * @param {Arguments} args (optional) Arguments array of a functions
 * @see EvalCodeHost
 */
function ExScriptParseError (msg, code, ex, args) { ExScriptParseError.baseConstructor.call(this, msg, code, ex, args); }
SubClass(ExBase, ExScriptParseError);

/**
 * @class Exception class for server side script loading
 * @constructor
 * @base ExBase
 * @param {String} msg Description of exception
 * @param {Number} code (optional) Error code
 * @param {Exception} ex (optional) An earlier exception to wrap
 * @param {Arguments} args (optional) Arguments array of a functions
 * @see EvalCodeHost
 */
function ExScriptLoadError (msg, code, ex, args) { ExScriptLoadError.baseConstructor.call(this, msg, code, ex, args); }
SubClass(ExBase, ExScriptLoadError);

/**
 * @class Exception class for file reading
 * @constructor
 * @base ExBase
 * @param {String} msg Description of exception
 * @param {Number} code (optional) Error code
 * @param {Exception} ex (optional) An earlier exception to wrap
 * @param {Arguments} args (optional) Arguments array of a functions
 */
//TODO: generalize this into file IO operation exceptions
function ExFileLoadError (msg, code, ex, args) { ExFileLoadError.baseConstructor.call(this, msg, code, ex, args); }
SubClass(ExBase, ExFileLoadError);

/**
 * @class Container for code executed in the context of a dynamic webpage.
 *  Keeps script page variable/object scope locked to individual requests.
 * @constructor
 * @param {JsWs} server JsWs server instance
 * @param {Request} request Incoming request object
 * @param {Response} response Outgoing response object
 * @param {ScriptContext} script Scripting context object
 */
function EvalCodeHost (server, request, response, script) {
	/** @type JsWs */
	this.server = server;
	/** @type Request */
	this.request = request;
	/** @type Response */
	this.response = response;
	/** @type ScriptContext */
	this.script = script;
}


/**
 * Runs cleanup on internal objects
 */
EvalCodeHost.prototype.destroy = function () {
	this.server = null;
	this.request = null;
	this.response = null;
	this.script = null;
}


/**
 * Parses a string that contains a mix of static text and/or
 * embedded script into pure JavaScript ready to be eval'd
 * @param {String} code String to be parsed
 * @returns JavaScript code ready to be evaluated
 * @type String
 * @exception ExScriptParseError
 */
EvalCodeHost.prototype.parse = function (code) {
	var codeParsed = "";

	var codeParts = code.split(/<\?js\s*/);

	for (var i = 0; i < codeParts.length; i++) {
		if (!codeParts[i].length) { continue; }

		isCodeEnd = (i == 0 || i == codeParts.length);

		codeSectionParts = codeParts[i].split(/\s*\?>/);
		if (isCodeEnd && codeSectionParts.length == 1) {
			codeSectionParts.unshift("");
		}
		if (codeSectionParts.length != 2) { throw(new ExScriptParseError('Unmatched code block markers near: "' + ((codeParts[i].length > 128) ? (codeParts[i].substring(0, 128) + " ...") : codeParts[i]) + '"')); }

		if (codeSectionParts[0].length) {
			if (codeSectionParts[0].match(/^\s*\=/)) {
				codeParsed += "response.write(" + codeSectionParts[0].replace(/^\s*=\s*/, '').replace(/;+\s*$/, '') + ");\n";
			} else {
				codeParsed += codeSectionParts[0] + "\n";
			}
		}

		if (codeSectionParts[1].length) {
			codeParsed += "response.write(\"" + codeSectionParts[1].replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/"/g, '\\"').replace(/\r/g, '\\r').replace(/\n/g, '\\n').replace(/\t/g, '\\t') + "\");\n";
		}
	}

	return(codeParsed);
}


/** Not implemented yet */
EvalCodeHost.prototype.streamTransfer = function (inStream, outStream, size) {
	// TODO: Support output stream parameter for streaming of URL
}


/**
 * Reads a file from disk
 * @param {VirtualPath|String} filepath Path to the file to open
 * @returns Contents of the file
 * @type String
 * @exception ExFileLoadError
 * @exception ExParamError
 */
EvalCodeHost.prototype.readFile = function (filepath) {
	if ((typeof filepath == "string") || (filepath instanceof String))
		filepath = new this.script.VirtualPath(filepath);
	if (filepath instanceof VirtualPath)
		filepath = this.script.mapPath(filepath)
	if (!(filepath instanceof Ci.nsILocalFile))
		throw(new ExParamError("Parameter \"filepath\" is of an unexpected type (" + typeof filepath + ")", 0, null, arguments));

	if (!filepath.exists() || !filepath.isFile()) {
		throw(new ExFileLoadError("Unable to load file \"" + filepath.path + "\"", 0, null, arguments));
	}

	var fileStream = Cc["@mozilla.org/network/file-input-stream;1"].
		createInstance(Ci.nsIFileInputStream);
	fileStream.init(filepath, 1, 0, false);
	var fileBinStream = Cc["@mozilla.org/binaryinputstream;1"].
		createInstance(Ci.nsIBinaryInputStream);
	fileBinStream.setInputStream(fileStream);
	var fileContents = fileBinStream.readBytes(filepath.fileSize);
	
	filepath = null;
	fileBinStream = null;
	fileStream.close();
	fileStream = null;

	return(fileContents);
}


/**
 * Loads script file and evaluates it. Errors reading the
 *  source file results in an exception.
 * @param {VirtualPath|String} filepath Path to the file to include
 * @param {Boolean} rawJs (optional) Flag to indicate that
 *  the file to be included is a plain JavaScript file with
 *  no containing script tags. This is typically used when a
 *  dynamic webpage needs to include a JavaScript library
 *  written for the client side
 * @exception ExScriptLoadError
 * @exception ExParamError
 */
EvalCodeHost.prototype.require = function (filepath, rawJs) {
	if ((typeof filepath == "string") || (filepath instanceof String))
		filepath = new this.script.VirtualPath(filepath);
	if (filepath instanceof VirtualPath)
		filepath = this.script.mapPath(filepath)
	if (!(filepath instanceof Ci.nsILocalFile))
		throw(new ExParamError("Parameter \"filepath\" is of an unexpected type (" + typeof filepath + ")", 0, null, arguments));

	rawJs = !!rawJs;

	var fileContents;
	try {
		fileContents =
			(rawJs ? '<?js' : '') +
			this.readFile(filepath)
			+ (rawJs ? '?>' : '');
	} catch (ex) {
		throw(new ExScriptLoadError("Unable to load include \"" + filepath.path + "\"", 0, ex, arguments));
	}

	this.eval(this.parse(fileContents));
}


/**
 * Same as {@link #require} except that errors reading the
 *  source file do not result in an exception. Failure is
 *  instead communicated by the return value
 * @param {VirtualPath|String} filepath Path to the file to include
 * @param {Boolean} rawJs (optional) Flag to indicate that
 *  the file to be included is a plain JavaScript 
 * @returns True/False indicating success in reading the file
 * @type Boolean
 * @see #require
 */
EvalCodeHost.prototype.include = function (filepath, rawJs) {
	try {
		this.require(filepath, rawJs);
	} catch (e) {
		if (!(e instanceof ExScriptLoadError)) { throw(e); }
		return(false);
	}
	return(true);
}


/**
 * Opens a URL and creates a stream to be read from
 * @param {String} url URL to open
 * @param {Number} depth (optional) Number of redirects to allow before giving up
 * @returns An array containing the channel object and an open stream
 * @type [nsIHttpChannel, nsIBinaryInputStream]
 * @exception ExFileLoadError
 */
EvalCodeHost.prototype.openNetFile = function (url, depth) {
	if (depth == null)
		depth = 5;

	var ioService = Cc["@mozilla.org/network/io-service;1"].
		getService(Ci.nsIIOService);
	var oUrl = ioService.newURI(url, null, null);
	var channel = ioService.newChannelFromURI(oUrl);
	// TODO: check for other supported channel types
	if (!(channel instanceof Ci.nsIHttpChannel)) { throw(new ExFileLoadError("Unable to open file due to unsupported protocol/channel", 0, null, arguments)); }
	var channelHttp = channel.QueryInterface(Ci.nsIHttpChannel);
	channelHttp.referrer = oUrl;
	var inStream = Cc["@mozilla.org/binaryinputstream;1"].
		createInstance(Ci.nsIBinaryInputStream);
	inStream.setInputStream(channelHttp.open());
	if (!channelHttp.requestSucceeded) {
		if (channelHttp.responseStatus == "302" || channelHttp.responseStatus == "301")
			if (depth > 1)
				return(this.openNetFile(channelHttp.getResponseHeader("location"), depth - 1));
			else
				throw(new ExFileLoadError("Unable to load file due to redirection limit", 0, null, arguments));
		else
			throw(new ExFileLoadError("Unable to load file due to failed request", 0, null, arguments));
	}
	return({
		"channel" : channelHttp,
		"stream" : inStream
		});
}


/**
 * Reads a file from the network
 * @param {String} url URL to open
 * @returns Body of the response
 * @type String
 * @exception ExFileLoadError
 */
EvalCodeHost.prototype.readNetFile = function (url) {
	var netFile;
	var sContent = "";

	try {
		// TODO: don't assume http channel
		netFile = this.openNetFile(url);
		if (!netFile.channel.requestSucceeded) { throw(new ExFileLoadError("Unable to load file due to failed request", 0, null, arguments)); }
		while (netFile.stream.available() || sContent.length < netFile.channel.contentLength) {
			sContent += netFile.stream.readBytes(netFile.stream.available());
		}
		netFile.stream.close();
		netFile.stream = null;
	} catch (ex) {
		if (netFile.stream != null) {
			netFile.stream.close();
			netFile.stream = null;
		}
		throw(ExFileLoadError("Unable to load file", 0, ex, arguments));
	}

	return(sContent);
}

/**
 * Creates missing directories in a path
 * @param {VirtualPath|String} directory Path to build out
 * @exception ExParamError
 */
EvalCodeHost.prototype.createPath = function (directory) {
	if ((typeof directory == "string") || (directory instanceof String))
		directory = new this.script.VirtualPath(directory);
	if (directory instanceof VirtualPath)
		directory = this.script.mapPath(directory)
	if (!(directory instanceof Ci.nsILocalFile))
		throw(new ExParamError("Parameter \"directory\" is of an unexpected type (" + typeof directory + ")", 0, null, arguments));

	if (directory.exists())
		return;
	
	/*
	 * nsIFile.parent is broken. When the path is "/", parent should be
	 * null but it turns into "/.." instead and parent.parent is "/../.." etc...
	 */
	var depth = 0;
	var dirs = new Array(directory);
	var tmpDir = directory.parent;
	while (depth++ < 32 && tmpDir != null && tmpDir.path != VirtualPath.sysRoot) {
		dirs.push(tmpDir);
		tmpDir = tmpDir.parent;
	}
	while (dirs.length) {
		var dir = dirs.pop();
		if (!dir.exists())
			/*
			 * Not happy with the 777. Perhaps do 0x0750 when
			 * VirtualPath.sysSeparator == "/" and 0x0777 when "\"
			 */
			//dir.create(1 /* DIRECTORY_TYPE */, 0x0755);
			//dir.create(Ci.nsIFile.DIRECTORY_TYPE, null);
			dir.create(Ci.nsIFile.DIRECTORY_TYPE, 0x0777);
	}
}

/**
 * Copies a file
 * @param {VirtualPath|String} file Path to the source file
 * @param {VirtualPath|String} directory Path to copy the file to
 * @param {Boolean} overwrite indicating whether existing files should be overwritten
 * @returns True/False indicating copy was a success
 * @type Boolean
 * @exception ExParamError
 */
EvalCodeHost.prototype.copyFile = function (file, directory, overwrite) {
	overwrite = (overwrite == null) ? false : overwrite;

	if ((typeof file == "string") || (file instanceof String))
		file = new this.script.VirtualPath(file);
	if (file instanceof VirtualPath)
		file = this.script.mapPath(file)
	if (!(file instanceof Ci.nsILocalFile))
		throw(new ExParamError("Parameter \"file\" is of an unexpected type (" + typeof file + ")", 0, null, arguments));

	if ((typeof directory == "string") || (directory instanceof String))
		directory = new this.script.VirtualPath(directory);
	if (directory instanceof VirtualPath)
		directory = this.script.mapPath(directory)
	if (!(directory instanceof Ci.nsILocalFile))
		throw(new ExParamError("Parameter \"directory\" is of an unexpected type (" + typeof directory + ")", 0, null, arguments));

	if (!file.exists())
		return(false);

	var destFile = directory.clone();
	destFile.append(file.leafName);

	if (destFile.exists())
		if (!overwrite)
			return(false);
		else
			destFile.remove(true);

	this.createPath(directory);

	file.copyTo(directory, "");

	return(true);
}
